Skip to content

Update AGP and dependencies#19

Open
juangardi21 wants to merge 2 commits intomainfrom
task/ANDROID-17464-update-agp
Open

Update AGP and dependencies#19
juangardi21 wants to merge 2 commits intomainfrom
task/ANDROID-17464-update-agp

Conversation

@juangardi21
Copy link
Copy Markdown

@juangardi21 juangardi21 commented Mar 27, 2026

🎟️ Jira ticket

ANDROID-17464

🥅 What's the goal?

Migrate the Gradle plugin from deprecated AGP internal APIs to the modern stable APIs, ensuring compatibility with newer AGP versions.

Before merging this PR, I will create an RC1 and try this new version in other projects

🚧 How do we do it?

  • Replace TestedExtension with AndroidComponentsExtension + ApplicationAndroidComponentsExtension (new variant API) to resolve applicationId and adb path.
  • Collect applicationId per variant at configuration time using onVariants {} as a lazy Provider<String>.
  • Remove all ddmlib usage (IDevice, FileListingService, CollectingOutputReceiver) in DeviceFileManager and replace with direct adb subprocess calls via ProcessBuilder.

📘 Documentation changes?

  • No docs to update nor create

🧪 How can I test this?

Run the connected Android tests with and without record mode to verify snapshots are pulled and reports are generated correctly.

Compilation Tests running in SW
Grabacion.de.pantalla.2026-03-30.a.las.16.21.22.mov
Grabacion.de.pantalla.2026-03-30.a.las.16.23.33.mov

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
throw AndroidSnaptestingNoDeviceProviderInstrumentTestTasksException()
}

val extension = project.extensions.findByType(TestedExtension::class.java)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGP 9.x removed TestedExtension from the public API. We now use the stable
variant API (AndroidComponentsExtension / ApplicationAndroidComponentsExtension) which is the recommended way to interact with Android build variants since AGP 7.x.

.firstOrNull { it.name == deviceProviderTask.variantName }
?: throw RuntimeException("TestVariant not found for ${deviceProviderTask.variantName}")
val applicationIdProvider = providerFactory.provider { testedVariant.applicationId }
val adbExecutablePath = extension.adbExecutable.absolutePath
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adbExecutable was removed from TestedExtension in AGP 9.x.
sdkComponents.adb is the stable replacement: it returns a lazy Provider pointing to the adb binary resolved from the configured SDK.

it.iDevice.executeShellCommand("rm -rf ${getDeviceAndroidSnaptestingRootAbsolutePath()}", receiver)
println(receiver.output)
devices.forEach { device ->
runAdb(device.serialNumber, "shell", "rm", "-rf", getDeviceAndroidSnaptestingRootAbsolutePath())
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously used DDMLib's IDevice.executeShellCommand() to run 'rm -rf' on the device. Now delegates to runAdb(), which spawns an 'adb -s shell rm -rf ...' subprocess.


private fun getDeviceAndroidSnaptestingRootAbsolutePath(): String =
"${FileListingService.DIRECTORY_SDCARD}/Download/android-snaptesting/$applicationId"
"/sdcard/Download/android-snaptesting/$applicationId"
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileListingService.DIRECTORY_SDCARD was a DDMLib constant ("/sdcard"). Using the literal string directly removes the DDMLib dependency entirely.

destinationPath: String,
) {
val fileEntry = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice).toFileEntry()
val remotePath = getDeviceAndroidSnaptestingSubfolderAbsolutePath(androidSnaptestingSubFolderInDevice)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old implementation built a FileEntry tree using DDMLib's FileListingService and then called IDevice.pullFile() for each entry.
The new implementation runs adb shell ls <remotePath> to list files and
adb pull <remote> <local> for each one, filtering out error lines from ls in case the folder doesn't exist yet on the device.

println(output)
}

private fun runAdbCapture(serial: String, vararg args: String): String {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runAdbCapture spawns a subprocess with the adb binary, targeting a specific device by serial (-s flag), merges stderr into stdout, and waits up to 60 seconds.

Copy link
Copy Markdown
Contributor

@jeprubio jeprubio Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably consider adding error handling for ADB command failures (e.g., check exit codes, handle timeouts, log errors) to improve robustness.”
Perhaps something like:

private fun runAdbCapture(
    serial: String,
    vararg args: String,
    throwOnError: Boolean = false
): AdbResult {
    val command = buildList {
        add(adbExecutablePath)
        add("-s")
        add(serial)
        addAll(args.toList())
    }
    try {
        val process = ProcessBuilder(command)
            .redirectErrorStream(false)
            .start()
        val output = process.inputStream.bufferedReader().readText()
        val error = process.errorStream.bufferedReader().readText()
        val finished = process.waitFor(60, TimeUnit.SECONDS)
        val exitCode = process.exitValue()
        if (!finished || exitCode != 0) {
            val message = "ADB command failed: ${command.joinToString(" ")}\nExit code: $exitCode\nOutput: $output\nError: $error"
            if (throwOnError) throw RuntimeException(message)
            else println(message)
        }
        return AdbResult(output, error, exitCode)
    } catch (e: Exception) {
        val message = "Exception running ADB command: ${command.joinToString(" ")}\n${e.message}"
        if (throwOnError) throw RuntimeException(message, e)
        else println(message)
        return AdbResult("", e.message ?: "", -1)
    }
}

private data class AdbResult(val output: String, val error: String, val exitCode: Int)

And then using it as: val result = runAdbCapture(serial, *args, throwOnError = true) in runAdb

) {
device.fileListingService.getChildrenSync(androidSnaptestingDeviceFolder).forEach {
device.pullFile(it.fullPath, "$destinationPath/${it.name}")
private fun runAdb(serial: String, vararg args: String) {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convenience wrapper that additionally prints the output to the Gradle log. Both replace the DDMLib IDevice shell/pull APIs removed in AGP 9.x.

@juangardi21 juangardi21 requested review from jeprubio and jeslat March 31, 2026 09:52
Copy link
Copy Markdown
Contributor

@jeprubio jeprubio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! 🚀


project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)
?.onVariants { variant ->
// variant.name == "debug" → test task variant name == "debugAndroidTest"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment in the code necessary?

println(output)
}

private fun runAdbCapture(serial: String, vararg args: String): String {
Copy link
Copy Markdown
Contributor

@jeprubio jeprubio Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably consider adding error handling for ADB command failures (e.g., check exit codes, handle timeouts, log errors) to improve robustness.”
Perhaps something like:

private fun runAdbCapture(
    serial: String,
    vararg args: String,
    throwOnError: Boolean = false
): AdbResult {
    val command = buildList {
        add(adbExecutablePath)
        add("-s")
        add(serial)
        addAll(args.toList())
    }
    try {
        val process = ProcessBuilder(command)
            .redirectErrorStream(false)
            .start()
        val output = process.inputStream.bufferedReader().readText()
        val error = process.errorStream.bufferedReader().readText()
        val finished = process.waitFor(60, TimeUnit.SECONDS)
        val exitCode = process.exitValue()
        if (!finished || exitCode != 0) {
            val message = "ADB command failed: ${command.joinToString(" ")}\nExit code: $exitCode\nOutput: $output\nError: $error"
            if (throwOnError) throw RuntimeException(message)
            else println(message)
        }
        return AdbResult(output, error, exitCode)
    } catch (e: Exception) {
        val message = "Exception running ADB command: ${command.joinToString(" ")}\n${e.message}"
        if (throwOnError) throw RuntimeException(message, e)
        else println(message)
        return AdbResult("", e.message ?: "", -1)
    }
}

private data class AdbResult(val output: String, val error: String, val exitCode: Int)

And then using it as: val result = runAdbCapture(serial, *args, throwOnError = true) in runAdb

# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true No newline at end of file
# Non-transitive R classes are the default in AGP 9.x and above
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment necessary?

Comment on lines +17 to +18
// Collect applicationId per test-variant name at configuration time using the new variant API.
// onVariants runs during project configuration, before afterEvaluate.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, this comment is more of a PR comment than a comment that should be in the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants